/*---------------------------------
#  author: chenzhen
#  foxmail: bit57chenzhen@foxmail.com
#  date: 2020.11.22
#  function: a player with FFmpeg and SDL2
---------------------------------*/

#include <stdio.h>
#include <libavcodec/avcodec.h>
#include <libavformat/avformat.h>
#include <libswscale/swscale.h>
#include <libavutil/log.h>
#include <libavutil/imgutils.h>
#include <SDL2/SDL.h>
#include <SDL2/SDL_video.h>
#include <SDL2/SDL_render.h>
#include <SDL2/SDL_rect.h>

int main(int argc, char *argv[]) {
    if (argc <2){
        av_log(NULL, AV_LOG_ERROR, "please input a file!\n");
        return -1;
    }

    int ret = 0;
    int video_idx = -1;
    int buffer_size = 0;
    uint8_t* buffer = NULL;
    AVFormatContext* p_fmt_ctx = NULL;
    AVCodecParameters* p_codec_par = NULL;
    AVCodec* p_codec = NULL;
    AVCodecContext* p_codec_ctx = NULL;
    AVPacket* p_packet = NULL;
    AVFrame* p_frame_raw = NULL;
    AVFrame* p_frame_yuv = NULL;
    struct SwsContext* p_sws_ctx = NULL;
    SDL_Window* p_sdl_window = NULL;
    SDL_Renderer* p_sdl_renderer = NULL;
    SDL_Texture* p_sdl_texture = NULL;
    SDL_Rect sdl_rect;

    p_fmt_ctx = avformat_alloc_context();
    if(p_fmt_ctx == NULL){
        av_log(NULL, AV_LOG_ERROR, "avformat_alloc_context ERROR\n");
        return -1;
    }

    ret = avformat_open_input(&p_fmt_ctx, argv[1], NULL, NULL);
    if(ret != 0){
        av_log(NULL, AV_LOG_ERROR, "avformat_open_input ERROR\n");
        return -1;
    }

    ret = avformat_find_stream_info(p_fmt_ctx, NULL);
    if(ret < 0 ){
        av_log(NULL, AV_LOG_ERROR, "avformat_find_stream_info ERROR\n");
        return -1;
    }

    av_dump_format(p_fmt_ctx, 0, NULL, 0);

    for (int i=0; i<p_fmt_ctx->nb_streams; i++){
        if(p_fmt_ctx->streams[i]->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
            video_idx = i;
            av_log(NULL, AV_LOG_INFO, "Find a video stream index: %d \n", i);
            break;
        }
    }

    if(video_idx == -1){
        av_log(NULL, AV_LOG_ERROR, "No video stream \n");
        return -1;
    }

    p_codec_par = p_fmt_ctx->streams[video_idx]->codecpar;
    p_codec = avcodec_find_decoder(p_codec_par->codec_id);
    if(p_codec == NULL){
        av_log(NULL, AV_LOG_ERROR, "avcodec_find_decoder ERROR\n");
        return -1;
    }

    p_codec_ctx = avcodec_alloc_context3(p_codec);
    if(p_codec_ctx == NULL){
        av_log(NULL, AV_LOG_ERROR, "avcodec_alloc_context3 ERROR\n");
        return -1;
    }

    ret = avcodec_parameters_to_context(p_codec_ctx, p_codec_par);
    if(ret < 0){
        av_log(NULL, AV_LOG_ERROR, "avcodec_parameters_to_context ERROR\n");
        return -1;
    }

    ret = avcodec_open2(p_codec_ctx, p_codec, NULL);
    if(ret < 0){
        av_log(NULL, AV_LOG_ERROR, "avcodec_open2 ERROR\n");
        return -1;
    }

    p_packet = av_packet_alloc();
    if(p_packet == NULL){
        av_log(NULL, AV_LOG_ERROR, "av_packet_alloc ERROR\n");
        return -1;
    }

    p_frame_raw = av_frame_alloc();
    if(p_frame_raw == NULL){
        av_log(NULL, AV_LOG_ERROR, "av_frame_alloc ERROR\n");
        return -1;
    }

    p_frame_yuv = av_frame_alloc();
    if(p_frame_yuv == NULL){
        av_log(NULL, AV_LOG_ERROR, "av_frame_alloc ERROR\n");
        return -1;
    }

    buffer_size = av_image_get_buffer_size(AV_PIX_FMT_YUV420P, 
                                            p_codec_ctx->width, 
                                            p_codec_ctx->height,
                                            1);

    if(buffer_size < 0){
        av_log(NULL, AV_LOG_ERROR, "av_image_get_buffer_size ERROR\n");
        return -1;
    }

    buffer = (uint8_t *) av_mallocz(buffer_size);
    if(buffer == NULL){
        av_log(NULL, AV_LOG_ERROR, "av_mallocz ERROR\n");
        return -1;
    }

    av_image_fill_arrays(p_frame_yuv->data,
                        p_frame_yuv->linesize,
                        buffer,
                        AV_PIX_FMT_YUV420P,
                        p_codec_ctx->width,
                        p_codec_ctx->height,
                        1);

    p_sws_ctx = sws_getContext(p_codec_ctx->width, 
                            p_codec_ctx->height, 
                            p_codec_ctx->pix_fmt, 
                            p_codec_ctx->width, 
                            p_codec_ctx->height,
                            AV_PIX_FMT_YUV420P,
                            SWS_BICUBIC, 
                            NULL, 
                            NULL,
                            NULL);

    
    if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO | SDL_INIT_TIMER)){
        av_log(NULL, AV_LOG_ERROR, "SDL_Init ERROR\n");
        return -1;
    }

    p_sdl_window = SDL_CreateWindow("MyPlayer", 
                    SDL_WINDOWPOS_UNDEFINED, 
                    SDL_WINDOWPOS_UNDEFINED,
                    p_codec_ctx->width, 
                    p_codec_ctx->height,
                    SDL_WINDOW_OPENGL);

    if(p_sdl_window == NULL){
        av_log(NULL, AV_LOG_ERROR, "SDL_CreateWindow ERROR\n");
        return -1;
    }

    p_sdl_renderer = SDL_CreateRenderer(p_sdl_window, -1, 0);

    if(p_sdl_renderer == NULL){
        av_log(NULL, AV_LOG_ERROR, "SDL_CreateRenderer ERROR\n");
        return -1;
    }

    p_sdl_texture = SDL_CreateTexture(p_sdl_renderer,
                                    SDL_PIXELFORMAT_IYUV,
                                    SDL_TEXTUREACCESS_STREAMING,
                                    p_codec_ctx->width,
                                    p_codec_ctx->height);

    if(p_sdl_texture == NULL){
        av_log(NULL, AV_LOG_ERROR, "SDL_CreateTexture ERROR\n");
        return -1;
    }

    sdl_rect.x = 0;
    sdl_rect.y = 0;
    sdl_rect.w = p_codec_ctx->width;
    sdl_rect.h = p_codec_ctx->height;

    while(av_read_frame(p_fmt_ctx, p_packet) >= 0){
        if(p_packet->stream_index == video_idx){
            ret = avcodec_send_packet(p_codec_ctx, p_packet);
            if(ret < 0){
                av_log(NULL, AV_LOG_ERROR, "avcodec_send_packet ERROR with return code: %d\n", ret);
                return -1;
            }

            while(ret >= 0){
                ret = avcodec_receive_frame(p_codec_ctx, p_frame_raw);
                if(ret == AVERROR(EAGAIN) || ret == AVERROR_EOF || ret == AVERROR(EINVAL) || ret == AVERROR_INPUT_CHANGED){
                    break;
                }else if(ret < 0){
                    av_log(NULL, AV_LOG_ERROR, "Error while receiving a frame from the decoder %s\n", av_err2str(ret));
                    return -1;
                }

                sws_scale(p_sws_ctx, 
                        (const uint8_t * const*)p_frame_raw->data,
                        p_frame_raw->linesize,
                        0,
                        p_codec_ctx->height,
                        p_frame_yuv->data,
                        p_frame_yuv->linesize);
            
                SDL_UpdateYUVTexture(p_sdl_texture, 
                                    &sdl_rect, 
                                    p_frame_yuv->data[0], 
                                    p_frame_yuv->linesize[0],
                                    p_frame_yuv->data[1], 
                                    p_frame_yuv->linesize[1],
                                    p_frame_yuv->data[2], 
                                    p_frame_yuv->linesize[2]);

                ret = SDL_RenderClear(p_sdl_renderer);
                if(ret == -1){
                    av_log(NULL, AV_LOG_ERROR, "SDL_RenderClear ERROR\n");
                    return -1;
                }

                ret = SDL_RenderCopy(p_sdl_renderer, p_sdl_texture, NULL, &sdl_rect);
                if(ret == -1){
                    av_log(NULL, AV_LOG_ERROR, "SDL_RenderCopy ERROR\n");
                    return -1;
                }

                SDL_RenderPresent(p_sdl_renderer);

                SDL_Delay(40);
            }
            /*
            ret = avcodec_receive_frame(p_codec_ctx, p_frame_raw);
            if(ret < 0){
                av_log(NULL, AV_LOG_ERROR, "avcodec_receive_frame ERROR with return code: %d\n", ret);
                return -1;
            }

            sws_scale(p_sws_ctx, 
                    (const uint8_t * const*)p_frame_raw->data,
                    p_frame_raw->linesize,
                    0,
                    p_codec_ctx->height,
                    p_frame_yuv->data,
                    p_frame_yuv->linesize);
            
            SDL_UpdateYUVTexture(p_sdl_texture, 
                                &sdl_rect, 
                                p_frame_yuv->data[0], 
                                p_frame_yuv->linesize[0],
                                p_frame_yuv->data[1], 
                                p_frame_yuv->linesize[1],
                                p_frame_yuv->data[2], 
                                p_frame_yuv->linesize[2]);

            ret = SDL_RenderClear(p_sdl_renderer);
            if(ret == -1){
                av_log(NULL, AV_LOG_ERROR, "SDL_RenderClear ERROR\n");
                return -1;
            }

            ret = SDL_RenderCopy(p_sdl_renderer, p_sdl_texture, NULL, &sdl_rect);
            if(ret == -1){
                av_log(NULL, AV_LOG_ERROR, "SDL_RenderCopy ERROR\n");
                return -1;
            }

            SDL_RenderPresent(p_sdl_renderer);

            SDL_Delay(40);
            */

            av_frame_unref(p_frame_raw);
        }
        av_packet_unref(p_packet);
    } 

    sws_freeContext(p_sws_ctx);
    avformat_free_context(p_fmt_ctx);
    avcodec_free_context(&p_codec_ctx);
    av_frame_free(&p_frame_yuv);
    av_frame_free(&p_frame_raw);
    av_packet_free(&p_packet);
    av_freep(buffer);
    SDL_Quit();

    return 0;
}